from __future__ import annotations

"""
策略管理路由（scripts）
----------------------
职责：
- 管理员：创建/修改/启停策略，旋转密钥并返回明文
- 普通用户：查看可用策略市场（仅激活与基础字段）

辅助：
- `_build_webhook_url` 生成带 url_token 的 webhook 地址
- `_default_payload_template` 提供示例请求体
- `_pine_snippet` 提供 TradingView alert 示例片段
"""

from typing import List

from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
from sqlalchemy import delete, select, update
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession

from ..db import get_session
from ..deps import require_admin, require_user
from ..models import AuditLog, MarketType, Script, ScriptStatus, ScriptSubscription
from ..schemas import ScriptCreateRequest, ScriptCreateResponse, ScriptResponse, ScriptUpdateRequest
from ..services.risk import enforce_whitelists
from ..services.utils import generate_token, hash_token, now_utc

router = APIRouter(prefix="/scripts", tags=["scripts"])


def _build_webhook_url(script: Script, url_token: str) -> str:
    """拼接 webhook 路径，供前端展示与复制。"""
    return f"/webhooks/tv/script/{script.webhook_id}?k={url_token}"


def _default_payload_template(script: Script, body_secret: str) -> dict:
    """返回最小可用的 Webhook 请求体模板。"""
    return {
        "secret": body_secret,
        "nonce": "<unique-nonce>",
        "trade_type": "BUY",
        "order_type": "MARKET",
        "position_action": "OPEN",
        "symbol": script.symbol,
        "market_type": script.market_type,
        "price": None,
        "target": None,
    }


def _pine_snippet(webhook_url: str, body_secret: str) -> str:
    """返回可直接放入 TradingView 的 alert 内容示例。"""
    return (
        f'alert("{{\\"secret\\":\\"{body_secret}\\",\\"nonce\\":time,'
        '\\"trade_type\\":\\"{{strategy.market_position}}\\",'
        '\\"order_type\\":\\"MARKET\\",'
        '\\"position_action\\":\\"OPEN\\",'
        '\\"symbol\\":\\"{{ticker}}\\",'
        '\\"price\\":{{close}} }}", alert.freq_once_per_bar_close)'
    )


@router.post(
    "",
    response_model=ScriptCreateResponse,
    status_code=status.HTTP_201_CREATED,
    summary="创建策略（管理员）",
    description="管理员创建策略，成功后返回一次性的 url_token 与 body_secret 明文以及 webhook_url。",
)
async def create_script(
    payload: ScriptCreateRequest,
    admin=Depends(require_admin),
    session: AsyncSession = Depends(get_session),
) -> ScriptCreateResponse:
    """创建策略。

    - 校验白名单（交易对/订单类型）
    - 生成 webhook_id/url_token/body_secret（仅此次返回明文）
    - 持久化与审计记录
    """
    enforce_whitelists("", payload.symbol, "MARKET")
    webhook_id = generate_token(12)
    url_token = generate_token(32)
    body_secret = generate_token(32)

    script = Script(
        name=payload.name,
        symbol=payload.symbol,
        market_type=payload.market_type,
        webhook_id=webhook_id,
        url_token_hash=hash_token(url_token),
        body_secret_hash=hash_token(body_secret),
        status=ScriptStatus.active,
    )
    session.add(script)
    session.add(
        AuditLog(
            actor_id=admin.id,
            action="script.create",
            target=f"script:{payload.name}",
            payload_json={"payload": payload.dict()},
        )
    )
    try:
        await session.commit()
    except IntegrityError as exc:
        await session.rollback()
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Script already exists") from exc
    await session.refresh(script)
    base = ScriptResponse.model_validate(script)
    return ScriptCreateResponse(
        **base.model_dump(),
        webhook_url=_build_webhook_url(script, url_token),
        body_secret=body_secret,
        url_token=url_token,
        payload_template=_default_payload_template(script, body_secret),
        pine_snippet=_pine_snippet(_build_webhook_url(script, url_token), body_secret),
    )


@router.get(
    "",
    response_model=List[ScriptResponse],
    summary="策略列表（管理员）",
    description="管理员查看全部策略，支持分页与按名称/交易对/市场类型/状态过滤。",
)
async def list_scripts(
    admin=Depends(require_admin),
    session: AsyncSession = Depends(get_session),
    page: int = Query(1, ge=1),
    page_size: int = Query(50, ge=1, le=200),
    name: str | None = Query(None),
    symbol: str | None = Query(None),
    market_type: MarketType | None = Query(None),
    status_: ScriptStatus | None = Query(None, alias="status"),
) -> List[ScriptResponse]:
    """管理员分页查询策略列表。"""
    stmt = select(
        Script.id,
        Script.name,
        Script.symbol,
        Script.market_type,
        Script.status,
        Script.created_at,
    )
    stmt = stmt.where(Script.hidden.is_(False))
    # filters
    if name:
        stmt = stmt.where(Script.name == name)
    if symbol:
        stmt = stmt.where(Script.symbol == symbol)
    if market_type:
        stmt = stmt.where(Script.market_type == market_type)
    if status_:
        stmt = stmt.where(Script.status == status_)

    stmt = stmt.order_by(Script.created_at.desc()).offset((page - 1) * page_size).limit(page_size)
    result = await session.execute(stmt)
    rows = result.all()
    return [
        ScriptResponse(
            id=row.id,
            name=row.name,
            symbol=row.symbol,
            market_type=row.market_type,
            status=row.status,
            created_at=row.created_at,
        )
        for row in rows
    ]


# FastAPI 不允许 204 响应携带 body；此处直接返回 200 空 JSON 以避免断言
@router.delete(
    "/{script_id}",
    status_code=status.HTTP_204_NO_CONTENT,
    summary="删除策略（管理员）",
    description="删除策略记录并清空订阅。保留信号与订单记录。写入审计日志便于追踪。",
)
async def delete_script(
    script_id: int,
    admin=Depends(require_admin),
    session: AsyncSession = Depends(get_session),
) -> Response:
    script = await session.get(Script, script_id)
    if script is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Script not found")

    await session.execute(
        update(ScriptSubscription)
        .where(ScriptSubscription.script_id == script_id)
        .values(enabled=False, hidden=True)
    )

    script.status = ScriptStatus.paused
    script.hidden = True
    session.add(script)

    session.add(
        AuditLog(
            actor_id=admin.id,
            action="script.delete",
            target=f"script:{script_id}",
            payload_json={
                "script_id": script_id,
                "name": script.name,
            },
        )
    )
    await session.commit()
    return Response(status_code=status.HTTP_204_NO_CONTENT)


@router.get(
    "/market",
    response_model=List[ScriptResponse],
    summary="策略市场（普通用户）",
    description="普通用户可见的策略列表，仅返回激活状态与基础字段。",
)
async def market_scripts(
    _: object = Depends(require_user),
    session: AsyncSession = Depends(get_session),
) -> List[ScriptResponse]:
    """普通用户可见策略（仅 active）。"""
    stmt = (
        select(
            Script.id,
            Script.name,
            Script.symbol,
            Script.market_type,
            Script.status,
            Script.created_at,
        )
        .where(Script.status == ScriptStatus.active, Script.hidden.is_(False))
    )
    result = await session.execute(stmt)
    rows = result.all()
    return [
        ScriptResponse(
            id=row.id,
            name=row.name,
            symbol=row.symbol,
            market_type=row.market_type,
            status=row.status,
            created_at=row.created_at,
        )
        for row in rows
    ]


@router.get(
    "/{script_id}",
    response_model=ScriptResponse,
    summary="策略详情（管理员）",
    description="管理员查看策略基础信息（不包含明文口令）。如需获取明文，请调用旋转接口。",
)
async def get_script(
    script_id: int,
    admin=Depends(require_admin),
    session: AsyncSession = Depends(get_session),
) -> ScriptResponse:
    """获取策略详情（不含明文凭据）。"""
    script = await session.get(Script, script_id)
    if not script:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Script not found")
    return ScriptResponse.model_validate(script)


@router.post(
    "/{script_id}/pause",
    response_model=ScriptResponse,
    summary="暂停策略（管理员）",
    description="将策略状态设置为 paused。",
)
async def pause_script(
    script_id: int,
    admin=Depends(require_admin),
    session: AsyncSession = Depends(get_session),
) -> ScriptResponse:
    """暂停策略（状态置为 paused）。"""
    script = await session.get(Script, script_id)
    if not script:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Script not found")
    script.status = ScriptStatus.paused
    session.add(
        AuditLog(
            actor_id=admin.id,
            action="script.pause",
            target=f"script:{script.id}",
            payload_json={"script_id": script_id},
        )
    )
    await session.commit()
    await session.refresh(script)
    return ScriptResponse.from_orm(script)


@router.post(
    "/{script_id}/resume",
    response_model=ScriptResponse,
    summary="恢复策略（管理员）",
    description="将策略状态设置为 active。",
)
async def resume_script(
    script_id: int,
    admin=Depends(require_admin),
    session: AsyncSession = Depends(get_session),
) -> ScriptResponse:
    """恢复策略（状态置为 active）。"""
    script = await session.get(Script, script_id)
    if not script:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Script not found")
    script.status = ScriptStatus.active
    session.add(
        AuditLog(
            actor_id=admin.id,
            action="script.resume",
            target=f"script:{script.id}",
            payload_json={"script_id": script_id},
        )
    )
    await session.commit()
    await session.refresh(script)
    return ScriptResponse.from_orm(script)


@router.put(
    "/{script_id}",
    response_model=ScriptResponse,
    summary="修改策略基础信息（管理员）",
    description="仅允许修改名称、交易对与市场类型。",
)
async def update_script(
    script_id: int,
    payload: ScriptUpdateRequest,
    admin=Depends(require_admin),
    session: AsyncSession = Depends(get_session),
) -> ScriptResponse:
    """更新策略基础字段（name/symbol/market_type）。"""
    script = await session.get(Script, script_id)
    if not script:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Script not found")

    if payload.name is not None:
        script.name = payload.name
    if payload.symbol is not None:
        enforce_whitelists("", payload.symbol, "MARKET")
        script.symbol = payload.symbol
    if payload.market_type is not None:
        script.market_type = payload.market_type

    session.add(
        AuditLog(
            actor_id=admin.id,
            action="script.update",
            target=f"script:{script.id}",
            payload_json={k: v for k, v in payload.model_dump().items() if v is not None},
        )
    )
    await session.commit()
    await session.refresh(script)
    return ScriptResponse.model_validate(script)


@router.post(
    "/{script_id}/rotate-secret",
    response_model=ScriptCreateResponse,
    summary="旋转口令并返回明文（管理员）",
    description="为该策略生成新的 url_token 与 body_secret，明文仅在本次响应返回并立即生效。",
)
async def rotate_secret(
    script_id: int,
    admin=Depends(require_admin),
    session: AsyncSession = Depends(get_session),
) -> ScriptCreateResponse:
    """旋转策略密钥并返回明文，用于替换旧的 webhook 参数。"""
    script = await session.get(Script, script_id)
    if not script:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Script not found")

    url_token = generate_token(32)
    body_secret = generate_token(32)
    script.url_token_hash = hash_token(url_token)
    script.body_secret_hash = hash_token(body_secret)
    await session.commit()
    await session.refresh(script)
    session.add(
        AuditLog(
            actor_id=admin.id,
            action="script.rotate_secret",
            target=f"script:{script.id}",
            payload_json={"script_id": script_id},
        )
    )
    await session.commit()

    base = ScriptResponse.model_validate(script)
    return ScriptCreateResponse(
        **base.model_dump(),
        webhook_url=_build_webhook_url(script, url_token),
        body_secret=body_secret,
        url_token=url_token,
        payload_template=_default_payload_template(script, body_secret),
        pine_snippet=_pine_snippet(_build_webhook_url(script, url_token), body_secret),
    )
